#include "FileSystem.h"

CFileSystem::CFileSystem(unsigned int userId)
{
	_userId = userId;
	sqlite3_open("filesystem.db",&_fsdb);
	sqlite3_exec(_fsdb, "CREATE TABLE IF NOT EXISTS fsInfo (Id INTEGER PRIMARY KEY AUTOINCREMENT, fsLabel TEXT, fsSize INTEGER, fsStatus INTEGER);", NULL, NULL,NULL);
	sqlite3_exec(_fsdb, "CREATE TABLE IF NOT EXISTS fsFiles (Id INTEGER PRIMARY KEY AUTOINCREMENT, fileName TEXT, fileType INTEGER, fileHidden INTEGER, fileReadOnly INTEGER, fileCreatedBy INTEGER, fileModifiedBy INTEGER, fileDeletedBy INTEGER, fileModifiedTime INTEGER, fileVersion INTEGER, fileDeleted INTEGER, fileLocked INTEGER, fileCurrentRevision INTEGER);", NULL, NULL,NULL);
	sqlite3_exec(_fsdb, "CREATE TABLE IF NOT EXISTS fsFileRevisions (Id INTEGER PRIMARY KEY AUTOINCREMENT, revisionFileId INTEGER, revisionVersion INTEGER, revisionFileSize INTEGER, revisionData BLOB, revisionDate INTEGER);", NULL, NULL,NULL);
	sqlite3_exec(_fsdb, "CREATE TABLE IF NOT EXISTS fsPermissions (Id INTEGER PRIMARY KEY AUTOINCREMENT, permissionFileId INTEGER, permissionUserId INTEGER, permission INTEGER);", NULL, NULL,NULL);
	sqlite3_exec(_fsdb, "CREATE INDEX IF NOT EXISTS idxFsInfo_Id on fsInfo (Id);", NULL, NULL,NULL);
	sqlite3_exec(_fsdb, "CREATE INDEX IF NOT EXISTS idxFsFiles_Id on fsFiles (Id);", NULL, NULL,NULL);
	sqlite3_exec(_fsdb, "CREATE INDEX IF NOT EXISTS idxFsFileRevisions_Id on fsFileRevisions (Id);", NULL, NULL,NULL);
	sqlite3_exec(_fsdb, "CREATE INDEX IF NOT EXISTS idxFsPermissions_Id on fsPermissions (Id);", NULL, NULL,NULL);
	sqlite3_exec(_fsdb, "CREATE INDEX IF NOT EXISTS idxFsFiles_Name on fsFiles (fileName);", NULL, NULL,NULL);
	sqlite3_exec(_fsdb, "CREATE INDEX IF NOT EXISTS idxFsFileRevisions_FileId on fsFileRevisions (revisionFileId,revisionVersion);", NULL, NULL,NULL);
	sqlite3_exec(_fsdb, "CREATE INDEX IF NOT EXISTS idxFsPermissions_FileId on fsPermissions (permissionFileId,permissionUserId);", NULL, NULL,NULL);
	sqlite3_exec(_fsdb, "PRAGMA synchronous = OFF;", NULL, NULL,NULL);
}

CFileSystem::~CFileSystem()
{
	if(_fsdb){ sqlite3_close(_fsdb);}
}

CFile* CFileSystem::getFile(const char* fileName, unsigned int &fileContentSize)
{

	CFile *f = new CFile();
	f->fileContents = 0;
	f->fileSize = 0;
	f->errMessage = "";

	unsigned int fileId = getFileId(fileName);
	if(fileId > 0)
	{
		/* now complete the return */
		unsigned int fileRevisionId = getLatestRevision(fileId);
		if(fileRevisionId > 0)
		{

			/* check that this user has permission to this file */
			if(getFilePermissions(fileId,_userId) & PERMISSION_READ)
			{
				f->errorStatus = FS_NOERROR;
				f->setAsString(string(getDataForRevision(fileRevisionId, f->fileSize)));
			}
			else
			{
				f->errorStatus = FS_PERMISSIONSERR;
				f->errMessage = "access denied, no read permission for this file";
				f->fileName = fileName;
			}
		}
		else
		{
			f->errorStatus = (char)fileRevisionId;
			f->fileName = fileName;
			f->errMessage = "revision not found for this file";
		}
	}
	else
	{

		// there was an error, return a content size of 0
		f->errorStatus = (char)fileId;
		f->fileName = fileName;
		
		switch(fileId)
		{
			case FS_FILENOTFOUND:
				f->errMessage = "file not found";
				break;
			case FS_FILESYSTEMERR:
				f->errMessage = "there was an error in the file system";
				break;
			case FS_PERMISSIONSERR:
				f->errMessage = "access denied to file";
				break;
		}

	}

	return f;
}

CFile* CFileSystem::putFile(CFile *f)
{
	CFile *r = new CFile();
	r->fileSize = 0;
	r->fileContents = 0;
	r->errMessage = "";

	int fileId = getFileId(f->fileName.c_str());
	if(fileId == FS_FILESYSTEMERR)
	{
		r->errMessage = "file not written, system error";
		r->errorStatus = FS_FILESYSTEMERR;
		r->fileName = f->fileName.c_str();
	}
	else
	{
		// ok to proceed to write the file
		if(fileId == FS_FILENOTFOUND)
		{
			/* insert a record for this file */
			char* sql = new char[MAX_FILE_QUERY_LEN];
			sprintf(sql, "INSERT INTO fsFiles (fileName,fileCreatedBy) VALUES ('%s',%u);", f->fileName.c_str(), _userId);
			sqlite3_exec(_fsdb, sql, NULL, NULL,NULL);
			delete [] sql;
			fileId = getFileId(f->fileName.c_str());

			/* now setup the initial permissions for that file */
			setFilePermissions(fileId, _userId, PERMISSION_LIST|PERMISSION_READ|PERMISSION_WRITE|PERMISSION_DELETE|PERMISSION_SECURITY);

			/* now we add the super users permissions for this file */
			setFilePermissions(fileId, 1, PERMISSION_LIST|PERMISSION_READ|PERMISSION_WRITE|PERMISSION_DELETE|PERMISSION_SECURITY);
		}
		
		if(fileId > 0)
		{
			if(getFilePermissions(fileId,_userId) & PERMISSION_WRITE)
			{
				// insert a new revision first then update the master file record, this needs to be done at a more base level
				sqlite3_stmt *stmt;
				const char* sql = "INSERT INTO fsFileRevisions (revisionFileId,revisionVersion,revisionFileSize,revisionData,revisionDate) VALUES (?,?,?,?,strftime('%s','now'));\0";
				sqlite3_prepare_v2(_fsdb, sql, strlen(sql), &stmt, NULL);
			
				int nextRevisionForFileId = getNextVersion(fileId);

				sqlite3_bind_int(stmt, 1, fileId);
				sqlite3_bind_int(stmt, 2, nextRevisionForFileId);
				sqlite3_bind_int(stmt, 3, f->fileSize);
				sqlite3_bind_blob(stmt, 4, (void*)(void*)f->fileContents, f->fileSize, SQLITE_TRANSIENT);

				bool working = true;

				while(working)
				{
					int result = sqlite3_step(stmt);
					switch(result)
					{
						case SQLITE_BUSY:
							Sleep(25);
							std::cout<<"sqlite database was busy, will retry"<<endl;
							break;
						case SQLITE_DONE:
							working = false;
							break;
						default:
							r->errorStatus = FS_FILESYSTEMERR;
							r->errMessage = "unable to commit file to the revisions store";
							r->fileName = f->fileName.c_str();
							working = false;
							break;
					}
				}

				sqlite3_finalize(stmt);

				/* now update the master record with the current information */
			
				const char * headerSql = "UPDATE fsFiles SET fileModifiedBy=?, fileModifiedTime=strftime('%s','now'), fileVersion=?, fileCurrentRevision=? WHERE Id=?";
				sqlite3_prepare_v2(_fsdb, headerSql, strlen(headerSql), &stmt, NULL);
				sqlite3_bind_int(stmt, 1, _userId);
				sqlite3_bind_int(stmt, 2, getLatestVersion(fileId));
				sqlite3_bind_int(stmt, 3, getLatestRevision(fileId));
				sqlite3_bind_int(stmt, 4, fileId);

				working = true;

				while(working)
				{
					int result = sqlite3_step(stmt);
					switch(result)
					{
						case SQLITE_BUSY:
							std::cout<<"sqlite database was busy, will retry"<<endl;
							Sleep(25);
							break;
						case SQLITE_DONE:
							working = false;
							r->errorStatus = FS_NOERROR;
							r->setAsString(f->getAsString());
							r->fileName = f->fileName.c_str();
							break;
						default:
							r->errorStatus = FS_FILESYSTEMERR;
							r->errMessage = "unable to update the file header after inserting a new revision";
							r->fileName = f->fileName.c_str();
							working = false;
							break;
					}
				}

				sqlite3_finalize(stmt);

				/* now perform a revision clean */
				cleanRevisionsForFile(fileId);
			}
			else
			{
				/* permissions error for writing */
				r->errMessage = "file not written, access denied";
				r->errorStatus = FS_FILESYSTEMERR;
				r->fileName = f->fileName.c_str();
			}
		}
	}

	return r;
}

int	CFileSystem::getFileId(const char* fileName)
{

	char* sql = new char[MAX_FILE_QUERY_LEN];
	sprintf(sql, "SELECT Id from fsFiles WHERE fileName = '%s' LIMIT 1" , fileName);

	char	 *err=0;
	int		 result=0;
	int	     rows=0;
	int		 cols=0;
	char	 **dataset;

	bool	 keeptrying = true;
		
	while(keeptrying)
	{
		result = sqlite3_get_table(_fsdb,sql,&dataset,&rows,&cols,&err);

		delete [] sql;

		switch(result)
		{
			case SQLITE_BUSY:
				std::cout<<"sqlite database was busy, will retry"<<endl;
				Sleep(25);
				break;
			case SQLITE_OK:
				sqlite3_free(err);
				if(rows)
				{
					int rv =  (int)atol(dataset[1]);
					sqlite3_free_table(dataset);
					return rv;
				}
				else
				{
					return (int)FS_FILENOTFOUND;
				}
			default:
				sqlite3_free(err);
				return (int)FS_FILESYSTEMERR;
		}
	}


	return (int)FS_FILESYSTEMERR;


}

int	CFileSystem::getLatestVersion(int fileId)
{

	// CREATE TABLE IF NOT EXISTS fsFileRevisions (Id INTEGER PRIMARY KEY AUTOINCREMENT, revisionFileId INTEGER, revisionVersion INTEGER, revisionFileSize INTEGER, revisionData BLOB);

	char* sql = new char[MAX_FILE_QUERY_LEN];
	sprintf(sql, "SELECT revisionVersion from fsFileRevisions WHERE revisionFileId = '%u' ORDER BY revisionVersion DESC LIMIT 1" , fileId);

	char	 *err=0;
	int		 result=0;
	int	     rows=0;
	int		 cols=0;
	char	 **dataset;

	bool	 keeptrying = true;
		
	while(keeptrying)
	{
		result = sqlite3_get_table(_fsdb,sql,&dataset,&rows,&cols,&err);

		delete [] sql;

		switch(result)
		{
			case SQLITE_BUSY:
				std::cout<<"sqlite database was busy, will retry"<<endl;
				Sleep(25);
				break;
			case SQLITE_OK:
				sqlite3_free(err);
				if(rows)
				{
					int rv = (int)atol(dataset[1]);
					sqlite3_free_table(dataset);
					return rv;
				}
				else
				{
					return (int)FS_NOREVISIONFORFILE;
				}
			default:
				return (int)FS_FILESYSTEMERR;
		}
	}


	return (int)FS_FILESYSTEMERR;
}

int	CFileSystem::getLatestRevision(int fileId)
{

	// CREATE TABLE IF NOT EXISTS fsFileRevisions (Id INTEGER PRIMARY KEY AUTOINCREMENT, revisionFileId INTEGER, revisionVersion INTEGER, revisionFileSize INTEGER, revisionData BLOB);

	char* sql = new char[MAX_FILE_QUERY_LEN];
	sprintf(sql, "SELECT Id from fsFileRevisions WHERE revisionFileId = '%u' ORDER BY revisionVersion DESC LIMIT 1" , fileId);

	char	 *err=0;
	int		 result=0;
	int	     rows=0;
	int		 cols=0;
	char	 **dataset;

	bool	 keeptrying = true;
		
	while(keeptrying)
	{
		result = sqlite3_get_table(_fsdb,sql,&dataset,&rows,&cols,&err);

		delete [] sql;

		switch(result)
		{
			case SQLITE_BUSY:
				std::cout<<"sqlite database was busy, will retry"<<endl;
				Sleep(25);
				break;
			case SQLITE_OK:
				sqlite3_free(err);
				if(rows)
				{
					int rv = (int)atol(dataset[1]);
					sqlite3_free_table(dataset);
					return rv;
				}
				else
				{
					return (int)FS_NOREVISIONFORFILE;
				}
			default:
				return (int)FS_FILESYSTEMERR;
		}
	}


	return (int)FS_FILESYSTEMERR;
}

int	CFileSystem::getNextVersion(int fileId)
{
	int lastVersion = getLatestVersion(fileId);
	if(lastVersion == FS_NOREVISIONFORFILE) { lastVersion = 0; }
	return lastVersion+1;
}

char* CFileSystem::getDataForRevision(int revision, unsigned int &fileSize)
{

	char* sql = new char[MAX_FILE_QUERY_LEN];
	sprintf(sql, "SELECT revisionData,revisionFileSize from fsFileRevisions WHERE Id = %i LIMIT 1" , revision);

	char	 *err=0;
	int		 result=0;
	int	     rows=0;
	int		 cols=0;
	char	 **dataset=0;

	bool	 keeptrying = true;
		
	while(keeptrying)
	{
		result = sqlite3_get_table(_fsdb,sql,&dataset,&rows,&cols,&err);

		delete [] sql;

		switch(result)
		{
			case SQLITE_BUSY:
				std::cout<<"sqlite database was busy, will retry"<<endl;
				Sleep(25);
				break;
			case SQLITE_OK:
				sqlite3_free(err);
				if(rows)
				{
					fileSize = (unsigned int)atol(dataset[3]);
					return dataset[2];
				}
				else
				{
					return 0;
				}
			default:
				return 0;
		}
	}

	return 0;
}

void CFileSystem::cleanRevisionsForFile(int fileId)
{
	char* sql = new char[MAX_FILE_QUERY_LEN];
	sprintf(sql, "DELETE FROM fsFileRevisions WHERE Id IN (SELECT Id FROM fsFileRevisions WHERE revisionFileId=%i ORDER BY revisionVersion DESC LIMIT 99999999 OFFSET %i);" , fileId, (int)MAX_FILE_REVISIONS);
	sqlite3_exec(_fsdb, sql, NULL,NULL,NULL);
	delete [] sql;
}

void CFileSystem::rollBackToVersion(int fileId, int version)
{
	char* sql = new char[MAX_FILE_QUERY_LEN];
	sprintf(sql, "DELETE FROM fsFileRevisions WHERE revisionFileId=%i AND revisionVersion > %i; UPDATE fsFiles SET fileCurrentRevision = (SELECT Id FROM fsFileRevisions WHERE revisionFileId = %i ORDER BY revisionVersion DESC LIMIT 1), fileVersion = (SELECT revisionVersion FROM fsFileRevisions WHERE revisionFileId = %i ORDER BY revisionVersion DESC LIMIT 1) WHERE Id = %i;" , fileId, version, fileId, fileId, fileId);
	sqlite3_exec(_fsdb, sql, NULL,NULL,NULL);
	delete [] sql;
}

void	CFileSystem::setFilePermissions(int fileId, int userId, int permission)
{
	/* remove existing permission then add in new one */

	// fsPermissions (Id INTEGER, permissionFileId INTEGER, permissionUserId INTEGER, permission INTEGER);

	char* sql = new char[MAX_FILE_QUERY_LEN];
	sprintf(sql, "DELETE FROM fsPermissions WHERE permissionFileId=%i AND permissionUserId = %i; INSERT INTO fsPermissions (permissionFileId,permissionUserId,permission) VALUES (%i,%i,%i)" , fileId, userId, fileId, userId, (int)permission);
	sqlite3_exec(_fsdb, sql, NULL,NULL,NULL);
	delete [] sql;
}

int	CFileSystem::getFilePermissions(int fileId, int userId)
{
	// fsPermissions (Id INTEGER PRIMARY KEY AUTOINCREMENT, permissionFileId INTEGER, permissionUserId INTEGER, permission INTEGER

	sqlite3_stmt* statement;

	if (sqlite3_prepare(_fsdb,"SELECT permission from fsPermissions WHERE permissionFileId = ? AND permissionUserId = ? LIMIT 1",-1,&statement,0) == SQLITE_OK)
	{	
		sqlite3_bind_int(statement,1,fileId);
		sqlite3_bind_int(statement,2,userId);
		if(sqlite3_step(statement) == SQLITE_ROW)
		{
			int permission = sqlite3_column_int(statement,0);
			sqlite3_finalize(statement);
			return permission;
		}

	}
	else
	{
		cout<<sqlite3_errmsg(_fsdb)<<endl;
	}

	/* no security record for this file, return with no permissions */

	sqlite3_finalize(statement);
	return 0;
}

